package com.five.fivecat;

import com.five.servlet.HeroRequest;
import com.five.servlet.HeroResponse;
import com.five.servlet.HeroServlet;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.stream.ChunkedNioFile;
import io.netty.util.CharsetUtil;

import javax.activation.MimetypesFileTypeMap;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Map;

/**
 * HeroCat服务端处理器
 * <p>
 * 1）从用户请求URI中解析出要访问的Servlet名称
 * 2）从nameToServletMap中查找是否存在该名称的key。若存在，则直接使用该实例，否则执行第3）步
 * 3）从nameToClassNameMap中查找是否存在该名称的key，若存在，则获取到其对应的全限定性类名，
 * 使用反射机制创建相应的serlet实例，并写入到nameToServletMap中，若不存在，则直接访问默认Servlet
 */
public class CustomHandler extends ChannelInboundHandlerAdapter {
    private Map<String, HeroServlet> nameToServletMap;//线程安全  servlet--> 对象
    private Map<String, String> nameToClassNameMap;//线程不安全  servlet--> 全限定名称

    public CustomHandler(Map<String, HeroServlet> nameToServletMap, Map<String, String> nameToClassNameMap) {
        this.nameToServletMap = nameToServletMap;
        this.nameToClassNameMap = nameToClassNameMap;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof HttpRequest) {
            HttpRequest request = (HttpRequest) msg;
            String uri = request.uri();
            if (uri.endsWith("html")
                    || uri.endsWith(".css")
                    || uri.endsWith(".js")
                    || uri.endsWith(".png")) {
                handleStaticResource(ctx, msg);
                return;
            }

            // 从请求中解析出要访问的Servlet名称
            //aaa/bbb/twoservlet?name=aa
            String servletName = "";
            if (uri.contains("?") && uri.contains("/")) {
                servletName = uri.substring(uri.lastIndexOf("/") + 1, uri.indexOf("?"));
            }


            HeroServlet servlet = new DefaultCustomServlet();
            //第一次访问，Servlet是不会被加载的
            //初始化加载的只是类全限定名称，懒加载
            //如果访问Servlet才会去初始化它对象
            if (nameToServletMap.containsKey(servletName)) {
                servlet = nameToServletMap.get(servletName);
            } else if (nameToClassNameMap.containsKey(servletName)) {
                // double-check，双重检测锁：为什么要在锁前判断一次，还要在锁后继续判断一次？
                if (nameToServletMap.get(servletName) == null) {
                    synchronized (this) {
                        if (nameToServletMap.get(servletName) == null) {
                            // 获取当前Servlet的全限定性类名
                            String className = nameToClassNameMap.get(servletName);
                            // 使用反射机制创建Servlet实例
                            servlet = (HeroServlet) Class.forName(className).newInstance();
                            // 将Servlet实例写入到nameToServletMap
                            nameToServletMap.put(servletName, servlet);
                        }
                    }
                }
            } //  end-else if

            // 代码走到这里，servlet肯定不空
            HeroRequest req = new HttpCustomRequest(request);
            HeroResponse res = new HttpCustomResponse(request, ctx);
            // 根据不同的请求类型，调用servlet实例的不同方法
            if (request.method().name().equalsIgnoreCase("GET")) {
                servlet.doGet(req, res);
            } else if (request.method().name().equalsIgnoreCase("POST")) {
                servlet.doPost(req, res);
            }
            ctx.close();
        }
    }

    private void handleStaticResource(ChannelHandlerContext ctx, Object msg) throws IOException {

        if (msg instanceof HttpRequest) {
            HttpRequest request = (HttpRequest) msg;
            // 状态为1xx的话，继续请求
            if (HttpUtil.is100ContinueExpected(request)) {
                FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
                ctx.writeAndFlush(response);
            }

            //处理错误或者无法解析的http请求
            if (!request.decoderResult().isSuccess()) {
                FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST,
                        Unpooled.copiedBuffer("请求失败", CharsetUtil.UTF_8));

                response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain;charset=UTF-8");
                ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
            }

            //这里设置了只允许GET请求
            if (request.method() != HttpMethod.GET) {
                DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FORBIDDEN);

                ByteBuf byteBuf = Unpooled.copiedBuffer("只允许GET请求", CharsetUtil.UTF_8);
                response.content().writeBytes(byteBuf);
                byteBuf.release();

                HttpUtil.setContentLength(response, response.content().readableBytes());
                ctx.channel().writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
            }

            String uri = request.uri();
            String path = this.getClass().getClassLoader().getResource("webapp").getPath() + uri;
            System.out.println(path);
            File file = new File(path);

            //设置不支持favicon.ico文件
            if ("/favicon.ico".equals(uri)) {
                return;
            }

            //文件没有发现设置404
            if (!file.exists()) {
                FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND,
                        Unpooled.copiedBuffer("访问路径错误", CharsetUtil.UTF_8));

                response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain;charset=UTF-8");
                ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
            }

            HttpResponse response = new DefaultHttpResponse(request.protocolVersion(), HttpResponseStatus.OK);
            //设置文件格式内容
            if (path.endsWith(".html")) {
                response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8");
            } else if (path.endsWith(".js")) {
                response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/x-javascript");
            } else if (path.endsWith(".css")) {
                response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/css;charset=UTF-8");
            } else if (path.endsWith(".png")) {
                response.headers().set(HttpHeaderNames.CONTENT_TYPE, "image/png;base64");
            }

            if (HttpUtil.isKeepAlive(request)) {
                response.headers().set(HttpHeaderNames.CONTENT_LENGTH, file.length());
                response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
            }
            ctx.write(response);

            RandomAccessFile ra = new RandomAccessFile(file, "r");
            if (ctx.pipeline().get(SslHandler.class) == null) {
                ctx.write(new DefaultFileRegion(ra.getChannel(), 0, ra.length()));
            } else {
                ctx.write(new ChunkedNioFile(ra.getChannel()));
            }

            ChannelFuture channelFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
            if (!HttpUtil.isKeepAlive(request)) {
                channelFuture.addListener(ChannelFutureListener.CLOSE);
            }
            ra.close();
        }
    }


    private void handleResource(ChannelHandlerContext ctx, Object msg, String resource) throws IOException {
        HttpRequest fullHttpRequest = (HttpRequest) msg;
        String url = this.getClass().getResource("/").getPath() + resource;
        File file = new File(url);
        if (!file.exists()) {
            handleNotFound(ctx, fullHttpRequest);
        }
        if (file.isDirectory()) {
            handleDirectory(ctx, fullHttpRequest, file);
            return;
        }
        handleFile(ctx, fullHttpRequest, file);
    }

    private void handleNotFound(ChannelHandlerContext ctx, HttpRequest msg) {
        ByteBuf content = Unpooled.copiedBuffer("URL not found", CharsetUtil.UTF_8);
        HttpResponse response = new DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.NOT_FOUND, content);
        ChannelFuture future = ctx.writeAndFlush(response);
        future.addListener(ChannelFutureListener.CLOSE);
    }

    private void handleDirectory(ChannelHandlerContext ctx, HttpRequest msg, File file) {
        StringBuilder sb = new StringBuilder();
        File[] files = file.listFiles();
        if (files != null) {
            for (File f : files) {
                if (f.isHidden() || !f.canRead()) {
                    continue;
                }
                String name = f.getName();
                sb.append(name).append("<br/>");
            }
        }
        ByteBuf buffer = ctx.alloc().buffer(sb.length());
        buffer.writeCharSequence(sb.toString(), CharsetUtil.UTF_8);
        FullHttpResponse response = new DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.OK, buffer);
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
        ChannelFuture future = ctx.writeAndFlush(response);
        future.addListener(ChannelFutureListener.CLOSE);
    }

    private void handleFile(ChannelHandlerContext ctx, HttpRequest msg, File file) throws IOException {
        RandomAccessFile raf = new RandomAccessFile(file, "r");
        HttpHeaders headers = getContentTypeHeader(file);
        HttpResponse response = new DefaultHttpResponse(msg.protocolVersion(), HttpResponseStatus.OK, headers);
        ctx.write(response);
        ctx.write(new DefaultFileRegion(raf.getChannel(), 0, raf.length()));
        ChannelFuture future = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
        future.addListener(ChannelFutureListener.CLOSE);
    }

    private HttpHeaders getContentTypeHeader(File file) {
        MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
        HttpHeaders headers = new DefaultHttpHeaders();
        String contentType = mimeTypesMap.getContentType(file);
        if (contentType.equals("text/plain")) {
            //由于文本在浏览器中会显示乱码，此处指定为utf-8编码
            contentType = "text/plain;charset=utf-8";
        }
        headers.set(HttpHeaderNames.CONTENT_TYPE, contentType);
        return headers;
    }


    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}
